import * as LoginManager from './loginManager.js';
import * as Main from '../ui/main.js';
import * as MessageTray from '../ui/messageTray.js';
+import * as ParentalControlsManager from './parentalControlsManager.js';
+import * as SystemActions from './systemActions.js';
+
+import {loadInterfaceXML} from './fileUtils.js';
+
+const TimerChildIface = loadInterfaceXML('org.freedesktop.MalcontentTimer1.Child');
+const TimerChildProxy = Gio.DBusProxy.makeProxyWrapper(TimerChildIface);
export const HISTORY_THRESHOLD_SECONDS = 14 * 7 * 24 * 60 * 60; // maximum time history entries are kept
const LIMIT_UPCOMING_NOTIFICATION_TIME_SECONDS = 10 * 60; // notify the user 10min before their limit is reached
/**
* A manager class which tracks total active/inactive time for a user, and
+ * signals when the user has reached their session time limit for actively
+ * using the device if the user has parental controls session limits set. When
+ * they are disabled, the wellbeing daily time limit is considered, and manager
* signals when the user has reached their daily time limit for actively using
* the device, if limits are enabled.
*
- * Active/Inactive time is based off the total time the user account has spent
+ * For parental controls session time limit, the manager tracks the active/inactive
+ * time via malcontent-timerd.
+ *
+ * For wellbeing daily time limit, the way in which the manager tracks the
+ * active/inactive time is based off the total time the user account has spent
* logged in to at least one active session, not idle (and not locked, but
* that’s a subset of idle time), and not suspended.
* This corresponds to the `active` state from sd_uid_get_state()
'daily-limit-reached': {},
},
}, class TimeLimitsManager extends GObject.Object {
- constructor(historyFile, clock, loginManagerFactory, loginUserFactory, settingsFactory) {
+ constructor(historyFile, clock, loginManagerFactory, loginUserFactory, settingsFactory, parentalControlsManagerFactory, timerChildProxyFactory) {
super();
// Allow these few bits of global state to be overridden for unit testing
return loginManager.getCurrentUserProxy();
},
};
+
+ this._parentalControlsManagerFactory = parentalControlsManagerFactory ?? {
+ new: ParentalControlsManager.getDefault,
+ };
+ this._parentalControlsManager = this._parentalControlsManagerFactory.new();
+
+ this._parentalControlsManager.connectObject(
+ 'session-limits-changed', () => this._onSessionLimitsChanged().catch(logError),
+ this);
+
+ this._estimatedTimes = [];
+ this._timerChildProxyFactory = timerChildProxyFactory ?? {
+ new: () => {
+ return TimerChildProxy(Gio.DBus.system,
+ 'org.freedesktop.MalcontentTimer1',
+ '/org/freedesktop/MalcontentTimer1',
+ (proxy, error) => {
+ if (error)
+ console.debug(`Failed to get TimerChild proxy: ${error}`);
+ },
+ null, /* cancellable */
+ Gio.DBusProxyFlags.DO_NOT_AUTO_START_AT_CONSTRUCTION
+ );
+ },
+ };
+
+ this._timerChildProxy = this._timerChildProxyFactory.new();
+
+ this._timerChildProxy.connectSignal('EstimatedTimesChanged',
+ () => this._updateEstimatedTimes().catch(logError));
+
this._settingsFactory = settingsFactory ?? {
new: Gio.Settings.new,
};
this._timeChangeId = 0;
this._clockOffsetSecs = 0;
this._ignoreClockOffsetChanges = false;
+ this._latestUsageEndSecs = 0;
// Start tracking timings
this._updateStateMachine();
}
+ async _recordUsage(startSecs, endSecs) {
+ console.debug('Recording usage to the parental controls daemon: ' +
+ `${this._unixToString(startSecs)} ${this._unixToString(endSecs)}`);
+ try {
+ await this._timerChildProxy.RecordUsageAsync(
+ [[startSecs, endSecs, 'login-session', '']]);
+ this._latestUsageEndSecs = endSecs;
+ } catch (e) {
+ if (e.matches(Gio.DBusError, Gio.DBusError.SERVICE_UNKNOWN)) {
+ // Limits should only be enabled when the daemon is available.
+ if (this._parentalControlsManager.sessionLimitsEnabled())
+ throw e;
+ else
+ console.debug('Parental controls timer daemon not available');
+ } else {
+ console.warn(`Failed to record usage: ${e.message}`);
+ }
+ }
+ }
+
+ async _onSessionLimitsChanged() {
+ // New session limits may mean that the state machine will need to start/stop.
+ this._updateStateMachine();
+
+ // Checkpoint current active usage to the parental controls timer daemon,
+ // so that it can make a correct estimate of the remaining screen time.
+ // Wellbeing monitoring uses the stored edge events (state transitions),
+ // whereas the parental controls timer daemon stores time periods only after
+ // one is finished, which is why it cannot know the start of the usage period,
+ // without recording it first. Recording will trigger EstimatedTimesChanged
+ // signal, which handler updates the estimated times and state.
+ if (this._stateTransitions.length > 0 &&
+ this._stateTransitions.at(-1).newState === UserState.ACTIVE) {
+ const startSecs = this._stateTransitions.at(-1).wallTimeSecs;
+ const endSecs = this.getCurrentTime();
+ await this._recordUsage(startSecs, endSecs);
+ }
+ }
+
+ async _updateEstimatedTimes() {
+ const [, timesSecs] = await this._timerChildProxy.GetEstimatedTimesAsync('login-session');
+ if (timesSecs[''])
+ this._estimatedTimes = timesSecs[''];
+ else
+ this._estimatedTimes = [];
+
+ this._updateState();
+ }
+
+ _storingTransitionsEnabled() {
+ return this._parentalControlsManager.sessionLimitsEnabled() ||
+ this._screenTimeLimitSettings.get_boolean('history-enabled');
+ }
+
_updateStateMachine() {
- if (!this._screenTimeLimitSettings.get_boolean('history-enabled')) {
+ if (!this._storingTransitionsEnabled()) {
if (this._state !== TimeLimitsState.DISABLED) {
this._stopStateMachine().catch(
e => console.warn(`Failed to stop state machine: ${e.message}`));
this._lastStateChangeTimeSecs = 0;
this.notify('state');
- if (this._screenTimeLimitSettings.get_boolean('history-enabled')) {
+ if (this._storingTransitionsEnabled()) {
// Add a fake transition to show the shutdown.
if (this._userState !== UserState.INACTIVE) {
const nowSecs = this.getCurrentTime();
* if the system real time clock changes (relative to the monotonic clock).
*/
async _loadTransitions() {
+ // When parental controls session limits are enabled, additionally update
+ // the cached estimated times from the parental controls timer daemon.
+ if (this._parentalControlsManager.sessionLimitsEnabled()) {
+ await this._updateEstimatedTimes();
+ console.debug('TimeLimitsManager: Loaded time estimates from the timer daemon');
+ }
+
const file = this._historyFile;
let contents;
this._stateTransitions = newTransitions;
+ // Record all of the usage records to the parental controls timer daemon
+ // if the parental controls session limits are in place.
+ if (this._parentalControlsManager.sessionLimitsEnabled()) {
+ for (var j = 0; j < this._stateTransitions.length - 1; j++) {
+ const start = this._stateTransitions[j];
+ const end = this._stateTransitions[j + 1];
+
+ // Make sure the transition is correct
+ if (start['newState'] !== UserState.ACTIVE ||
+ end['newState'] !== UserState.INACTIVE)
+ continue;
+
+ const startSecs = start['wallTimeSecs'];
+ const endSecs = end['wallTimeSecs'];
+
+ // Avoid re-sending the old state transitions
+ if (endSecs <= this._latestUsageEndSecs)
+ continue;
+
+ /* eslint-disable-next-line no-await-in-loop */
+ await this._recordUsage(startSecs, endSecs);
+ }
+ }
+
if (this._stateTransitions.length === 0) {
try {
await file.delete(this._cancellable);
if (startOfTodaySecs > this._lastStateChangeTimeSecs)
newState = TimeLimitsState.ACTIVE;
+ const [, currentSessionStart, currentSessionEnd, nextSessionStart] = this._estimatedTimes;
+ const sessionLimitsEnabled = this._parentalControlsManager.sessionLimitsEnabled();
+
+ if (sessionLimitsEnabled) {
+ // Parental controls session limits have either just been enabled
+ // and the estimated times have not been cached yet, or disabled
+ // but the manager has not detected that yet, so skip updating state.
+ if (nextSessionStart === undefined || currentSessionEnd === undefined)
+ return;
+ }
+
+ const sessionLimitsDebug = sessionLimitsEnabled ? 'current session start: ' +
+ `${this._unixToString(currentSessionStart)}, current session end: ` +
+ `${this._unixToString(currentSessionEnd)}` : 'disabled';
+ console.debug('TimeLimitsManager: Parental controls session limits: ' +
+ `${sessionLimitsDebug}`);
+
// Work out how much time the user has spent at the screen today.
const activeTimeTodaySecs = this._calculateActiveTimeTodaySecs(nowSecs, startOfTodaySecs);
const dailyLimitSecs = this._screenTimeLimitSettings.get_uint('daily-limit-seconds');
const dailyLimitEnabled = this._screenTimeLimitSettings.get_boolean('daily-limit-enabled');
const dailyLimitDebug = dailyLimitEnabled ? `${dailyLimitSecs}s` : 'disabled';
- console.debug('TimeLimitsManager: Active time today: ' +
+ console.debug('TimeLimitsManager: Wellbeing active time today: ' +
`${activeTimeTodaySecs}s, daily limit ${dailyLimitDebug}`);
- if (dailyLimitEnabled && activeTimeTodaySecs >= dailyLimitSecs) {
+
+ // Update TimeLimitsState. When the user is inactive, there's no point
+ // scheduling anything until they become active again.
+ if (sessionLimitsEnabled) {
+ if (nextSessionStart <= nowSecs) {
+ // Just entered daily schedule, so update cached estimated times
+ // which will perform state update afterwards.
+ this._updateEstimatedTimes();
+ return;
+ }
+
+ if (nowSecs >= currentSessionEnd) {
+ newState = TimeLimitsState.LIMIT_REACHED;
+
+ // Schedule an update for when the session limit will be reset again.
+ this._scheduleUpdateState(nextSessionStart - nowSecs);
+ } else if (this._userState === UserState.ACTIVE) {
+ newState = TimeLimitsState.ACTIVE;
+
+ // Schedule an update for when we expect the session limit will be reached.
+ this._scheduleUpdateState(currentSessionEnd - nowSecs);
+ }
+ } else if (dailyLimitEnabled && activeTimeTodaySecs >= dailyLimitSecs) {
newState = TimeLimitsState.LIMIT_REACHED;
- // Schedule an update for when the limit will be reset again.
+ // Schedule an update for when the daily limit will be reset again.
this._scheduleUpdateState(startOfTomorrowSecs - nowSecs);
} else if (this._userState === UserState.ACTIVE) {
newState = TimeLimitsState.ACTIVE;
- // Schedule an update for when we expect the limit will be reached.
+ // Schedule an update for when we expect the daily limit will be reached.
if (dailyLimitEnabled)
this._scheduleUpdateState(dailyLimitSecs - activeTimeTodaySecs);
} else {
// Update the saved state.
if (newState !== this._state) {
+ console.debug('TimeLimitsManager: State changed from ' +
+ `${timeLimitsStateToString(this._state)} ` +
+ `to ${timeLimitsStateToString(newState)}`);
this._state = newState;
this._lastStateChangeTimeSecs = nowSecs;
this.notify('state');
return this._state;
}
+ /**
+ * Whether the parental controls session limits are enabled.
+ *
+ * @type {boolean}
+ */
+ get sessionLimitsEnabled() {
+ return this._parentalControlsManager.sessionLimitsEnabled();
+ }
+
/**
* The time when the daily limit will be reached. If the user is currently
* active, and has not reached the limit, this is a non-zero value in the
/**
* Whether the daily limit is enabled.
*
- * If false, screen usage information is recorded, but no limit is enforced.
- *
* @type {boolean}
*/
get dailyLimitEnabled() {
* Glue class which takes the state-based output from TimeLimitsManager and
* converts it to event-based notifications for the user to tell them
* when their time limit has been reached. It factors the user’s UI preferences
- * into account.
+ * into account, and whether parental controls session limits are enabled.
*/
export const TimeLimitsDispatcher = GObject.registerClass(
class TimeLimitsDispatcher extends GObject.Object {
constructor(manager) {
super();
+ this._systemActions = new SystemActions.getDefault();
+
this._manager = manager;
this._manager.connectObject(
'notify::state', this._onStateChanged.bind(this),
}
case TimeLimitsState.LIMIT_REACHED: {
+ if (this._manager.sessionLimitsEnabled) {
+ // Trying to lock the screen will fail if the action is not
+ // available. This happens when admin disables lock screen,
+ // or when not running under GDM/logind. Those are corner cases
+ // which don’t need to be handled.
+ if (this._systemActions.canLockScreen)
+ this._systemActions.activateLockScreen();
+ break;
+ }
+
this._ensureEnabled();
if (this._manager.grayscaleEnabled) {
case TimeLimitsState.LIMIT_REACHED: {
// Notify the user that they’ve reached their limit, when we
- // transition from any state to LIMIT_REACHED.
- if (this._previousState !== TimeLimitsState.LIMIT_REACHED) {
+ // transition from any state to LIMIT_REACHED. Noop if parental
+ // controls session limits are enabled, since we’re locking then.
+ if (!this._manager.sessionLimitsEnabled &&
+ this._previousState !== TimeLimitsState.LIMIT_REACHED) {
this._ensureNotification({
title: _('Screen Time Limit Reached'),
body: _('It’s time to stop using the device'),
* of time, maintaining an internal ordered queue of events, and providing three
* groups of mock functions which the `TimeLimitsManager` uses to interact with
* it: mock versions of GLib’s clock and timeout functions, a mock proxy of the
- * logind `User` D-Bus object, and a mock version of `Gio.Settings`.
+ * logind `User` D-Bus object, a mock proxy around `Malcontent.Manager` object,
+ * a mock proxy of the malcontent-timerd `Child` D-Bus object, and a mock
+ * version of `Gio.Settings`.
*
* The internal ordered queue of events is sorted by time (in real/wall clock
* seconds, i.e. UNIX timestamps). On each _tick(), the next event is shifted
* daily time limit is reset at a specific time each morning.
*/
class TestHarness {
- constructor(settings, historyFileContents = null) {
+ constructor(settings, historyFileContents = null, sessionLimitsEnabled = false) {
this._currentTimeSecs = 0;
this._clockOffset = 100; // make the monotonic clock lag by 100s, arbitrarily
this._nextSourceId = 1;
this._settingsChangedDailyLimitSecondsCallback = null;
this._settingsChangedDailyLimitEnabledCallback = null;
this._settingsChangedGrayscaleCallback = null;
+ this._parentalControlsManagerSessionLimitsChangedCallback = null;
+ this._timerChildProxyEstimatedTimesChangedCallback = null;
// These two emulate relevant bits of the o.fdo.login1.User API
// See https://www.freedesktop.org/software/systemd/man/latest/org.freedesktop.login1.html#User%20Objects
this._historyFile = file;
+ this._sessionLimitsEnabled = sessionLimitsEnabled;
+ this._limitReached = false;
+ this._currentSessionStartTimeSecs = 0;
+ this._currentSessionEstimatedEndTimeSecs = GLib.MAX_UINT64;
+ this._nextSessionStartTimeSecs = GLib.MAX_UINT64;
+ this._nextSessionEstimatedEndTimeSecs = GLib.MAX_UINT64;
+
// And a mock D-Bus proxy for logind.
const harness = this;
});
}
+ /**
+ * Add a parental controls manager session limits changed event to the event
+ * queue. This simulates the `session-limits-changed` signal of the
+ * `Malcontent.Manager` object, notifying gnome-shell that the user’s
+ * session limits were changed, for example from malcontent-control.
+ *
+ * @param {string} timeStr Date/Time the event happens, in ISO 8601 format.
+ * @param {boolean} newSessionLimitsEnabled New session limits enabled setting.
+ */
+ addParentalControlsManagerSessionLimitsChangedEvent(timeStr, newSessionLimitsEnabled) {
+ return this._insertEvent({
+ type: 'parental-controls-manager-session-limits-changed',
+ time: TestHarness.timeStrToSecs(timeStr),
+ newSessionLimitsEnabled,
+ });
+ }
+
+ /**
+ * Add a timer child proxy estimated times changed event to the event
+ * queue. This simulates the `EstimatedTimesChanged` signal of the
+ * `org.freedesktop.MalcontentTimer1.Child` object, notifying gnome-shell
+ * that the user’s estimated session times were changed, for example as a
+ * result of gnome-shell recording usage.
+ *
+ * @param {string} timeStr Date/Time the event happens, in ISO 8601 format.
+ * @param {boolean} newLimitReached New limit reached setting.
+ * @param {number} newCurrentSessionStartTimeSecs New current session start
+ * time in seconds.
+ * @param {number} newCurrentSessionEstimatedEndTimeSecs New current session
+ * estimated end time in seconds.
+ * @param {number} newNextSessionStartTimeSecs New next session start time
+ * in seconds.
+ * @param {number} newNextSessionEstimatedEndTimeSecs New next session
+ * estimated end time in seconds.
+ */
+ addTimerChildProxyEstimatedTimesChangedEvent(timeStr, newLimitReached,
+ newCurrentSessionStartTimeSecs, newCurrentSessionEstimatedEndTimeSecs,
+ newNextSessionStartTimeSecs, newNextSessionEstimatedEndTimeSecs) {
+ return this._insertEvent({
+ type: 'timer-child-proxy-estimated-times-changed',
+ time: TestHarness.timeStrToSecs(timeStr),
+ newLimitReached,
+ newCurrentSessionStartTimeSecs,
+ newCurrentSessionEstimatedEndTimeSecs,
+ newNextSessionStartTimeSecs,
+ newNextSessionEstimatedEndTimeSecs,
+ });
+ }
+
/**
* Add a settings change event to the event queue. This simulates dconf
* notifying gnome-shell that the user has changed a setting, for example
});
}
+ /**
+ * Add a state assertion event to the event queue. This is a specialised
+ * form of `addAssertionEvent()` which asserts that the given
+ * start and end date/times of the most recent usage recording equal the
+ * expected values at date/time `timeStr`.
+ *
+ * @param {string} timeStr Date/Time to check the times at, in ISO 8601
+ * format.
+ * @param {string} startStr Start date/time in ISO 8601 format.
+ * @param {string} endStr End date/time in ISO 8601 format.
+ */
+ expectUsage(timeStr, startStr, endStr) {
+ return this.addAssertionEvent(timeStr, () => {
+ const [start, end] = this._timeStore;
+ expect(start)
+ .withContext('start')
+ .toEqual(TestHarness.timeStrToSecs(startStr));
+ expect(end)
+ .withContext('end')
+ .toEqual(TestHarness.timeStrToSecs(endStr));
+ });
+ }
+
_popEvent() {
return this._events.shift();
}
this.mockClock,
this.mockLoginManagerFactory,
this.mockLoginUserFactory,
- this.mockSettingsFactory);
+ this.mockSettingsFactory,
+ this.mockParentalControlsManagerFactory,
+ this.mockTimerChildProxyFactory);
}
/**
};
}
+ /**
+ * Get a mock parental controls manager factory for use in the
+ * `TimeLimitsManager` under test. This is an object providing constructors
+ * for `ParentalControlsManager` objects, which are proxies around the
+ * `Malcontent.Manager` objects. Each constructor returns a basic
+ * implementation of `ParentalControlsManager` which uses the daily limit
+ * and daily schedule variables passed to `TestHarness` in its constructor
+ * in the settings dictionary.
+ *
+ * This has an extra layer of indirection to match `mockSettingsFactory`.
+ */
+ get mockParentalControlsManagerFactory() {
+ return {
+ new: () => {
+ return {
+ connectObject: (...args) => {
+ const [
+ sessionLimitsChangedStr, sessionLimitsChangedCallback,
+ obj,
+ ] = args;
+
+ if (sessionLimitsChangedStr !== 'session-limits-changed' ||
+ typeof obj !== 'object')
+ fail('ParentalControlsManager.connectObject() not called in expected way');
+ if (this._parentalControlsManagerSessionLimitsChangedCallback !== null)
+ fail('ParentalControlsManager signals already connected');
+
+ this._parentalControlsManagerSessionLimitsChangedCallback = sessionLimitsChangedCallback;
+ },
+ sessionLimitsEnabled: () => {
+ return this._sessionLimitsEnabled;
+ },
+ };
+ },
+ };
+ }
+
+ /**
+ * Get a mock timer child proxy factory for use in the `TimeLimitsManager`
+ * under test. This is an object providing constructors for `TimerChildProxy`
+ * objects, which are proxies around the `org.freedesktop.MalcontentTimer1.Child`
+ * D-Bus API. Each constructor returns a basic implementation of `TimerChildProxy`.
+ *
+ * This has an extra layer of indirection to match `mockSettingsFactory`.
+ */
+ get mockTimerChildProxyFactory() {
+ return {
+ new: () => {
+ return {
+ connectSignal: (...args) => {
+ const [
+ estimatedTimesChangedStr, estimatedTimesChangedCallback,
+ ] = args;
+
+ if (estimatedTimesChangedStr !== 'EstimatedTimesChanged')
+ fail('TimerChildProxy.connectSignal() not called in expected way');
+ if (this._timerChildProxyEstimatedTimesChangedCallback !== null)
+ fail('TimerChildProxy signals already connected');
+
+ this._timerChildProxyEstimatedTimesChangedCallback = estimatedTimesChangedCallback;
+ },
+ RecordUsageAsync: args => {
+ return new Promise(resolve => {
+ const [[startSecs, endSecs]] = args;
+ this._timeStore = [startSecs, endSecs];
+ resolve();
+ });
+ },
+ GetEstimatedTimesAsync: _ => {
+ return new Promise(resolve => {
+ resolve([0, {
+ '': [
+ this._limitReached,
+ this._currentSessionStartTimeSecs,
+ this._currentSessionEstimatedEndTimeSecs,
+ this._nextSessionStartTimeSecs,
+ this._nextSessionEstimatedEndTimeSecs,
+ ],
+ }]);
+ });
+ },
+ };
+ },
+ };
+ }
+
/**
* Get a mock settings factory for use in the `TimeLimitsManager` under test.
* This is an object providing constructors for `Gio.Settings` objects. Each
if (this._loginUserPropertiesChangedCallback)
this._loginUserPropertiesChangedCallback();
break;
+ case 'parental-controls-manager-session-limits-changed':
+ this._sessionLimitsEnabled = event.newSessionLimitsEnabled;
+
+ if (this._parentalControlsManagerSessionLimitsChangedCallback)
+ this._parentalControlsManagerSessionLimitsChangedCallback();
+ break;
+ case 'timer-child-proxy-estimated-times-changed':
+ this._limitReached = event.newLimitReached;
+ this._currentSessionStartTimeSecs = event.newCurrentSessionStartTimeSecs;
+ this._currentSessionEstimatedEndTimeSecs = event.newCurrentSessionEstimatedEndTimeSecs;
+ this._nextSessionStartTimeSecs = event.newNextSessionStartTimeSecs;
+ this._nextSessionEstimatedEndTimeSecs = event.newNextSessionEstimatedEndTimeSecs;
+
+ if (this._timerChildProxyEstimatedTimesChangedCallback)
+ this._timerChildProxyEstimatedTimesChangedCallback();
+ break;
case 'settings-change':
this._settings[event.schemaId][event.key] = event.newValue;
harness.run();
});
+ it('cannot be disabled via GSettings if parental controls enabled', () => {
+ const harness = new TestHarness(
+ {
+ 'org.gnome.desktop.screen-time-limits': {
+ 'history-enabled': false,
+ 'daily-limit-enabled': false,
+ 'daily-limit-seconds': 4 * 60 * 60,
+ },
+ },
+ null,
+ {
+ 'parental-enabled': true,
+ }
+ );
+ harness.initializeMockClock('2024-06-01T10:00:00Z');
+ const timeLimitsManager = harness.createTimeLimitsManager();
+
+ harness.expectState('2024-06-01T10:00:01Z', timeLimitsManager, TimeLimitsState.ACTIVE);
+ harness.expectState('2024-06-01T15:00:00Z', timeLimitsManager, TimeLimitsState.ACTIVE);
+ harness.addLoginUserStateChangeEvent('2024-06-01T15:00:10Z', 'active', false);
+ harness.addLoginUserStateChangeEvent('2024-06-01T15:00:20Z', 'lingering', true);
+ harness.expectProperties('2024-06-01T15:00:30Z', timeLimitsManager, {
+ 'state': TimeLimitsState.ACTIVE,
+ });
+ harness.shutdownManager('2024-06-01T15:10:00Z', timeLimitsManager);
+
+ harness.run();
+ });
+
it('can be toggled on and off via GSettings', () => {
const harness = new TestHarness({
'org.gnome.desktop.screen-time-limits': {
harness.run();
});
+ it('tracks a single day’s usage with parental controls limit enabled', () => {
+ const harness = new TestHarness({
+ 'org.gnome.desktop.screen-time-limits': {
+ 'history-enabled': true,
+ 'daily-limit-enabled': true,
+ 'daily-limit-seconds': 4 * 60 * 60,
+ },
+ }, null, true);
+ harness.initializeMockClock('2024-06-01T10:00:00Z');
+ harness.addTimerChildProxyEstimatedTimesChangedEvent(
+ '2024-06-01T10:00:01Z',
+ false,
+ TestHarness.timeStrToSecs('2024-06-01T10:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-01T13:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-02T00:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-02T03:00:00Z')
+ );
+ harness.addParentalControlsManagerSessionLimitsChangedEvent(
+ '2024-06-01T10:00:02Z', true
+ );
+
+ const timeLimitsManager = harness.createTimeLimitsManager();
+
+ harness.expectState('2024-06-01T10:00:03Z', timeLimitsManager, TimeLimitsState.ACTIVE);
+ harness.expectState('2024-06-01T12:59:59Z', timeLimitsManager, TimeLimitsState.ACTIVE);
+ harness.expectState('2024-06-01T13:00:01Z', timeLimitsManager, TimeLimitsState.LIMIT_REACHED);
+ harness.shutdownManager('2024-06-01T13:00:02Z', timeLimitsManager);
+
+ harness.run();
+ });
+
it('tracks a single day’s usage early in the morning', () => {
const harness = new TestHarness({
'org.gnome.desktop.screen-time-limits': {
harness.run();
});
+ it('resets usage if the parental controls session limit is changed', () => {
+ const harness = new TestHarness({
+ 'org.gnome.desktop.screen-time-limits': {
+ 'history-enabled': true,
+ 'daily-limit-enabled': true,
+ 'daily-limit-seconds': 4 * 60 * 60,
+ },
+ }, null, true);
+ harness.initializeMockClock('2024-06-01T10:00:00Z');
+ harness.addTimerChildProxyEstimatedTimesChangedEvent(
+ '2024-06-01T10:00:01Z',
+ false,
+ TestHarness.timeStrToSecs('2024-06-01T10:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-01T13:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-02T00:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-02T03:00:00Z')
+ );
+ harness.addParentalControlsManagerSessionLimitsChangedEvent(
+ '2024-06-01T10:00:02Z', true
+ );
+ const timeLimitsManager = harness.createTimeLimitsManager();
+
+ // Run before the limit is reached
+ harness.expectState('2024-06-01T10:00:03Z', timeLimitsManager, TimeLimitsState.ACTIVE);
+ harness.expectState('2024-06-01T12:00:00Z', timeLimitsManager, TimeLimitsState.ACTIVE);
+
+ // Change session limits part-way through the day in a way which means
+ // the child still has time left that day
+ harness.addTimerChildProxyEstimatedTimesChangedEvent(
+ '2024-06-01T12:30:00Z',
+ false,
+ TestHarness.timeStrToSecs('2024-06-01T10:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-01T14:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-02T00:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-02T04:00:00Z')
+ );
+ harness.addParentalControlsManagerSessionLimitsChangedEvent(
+ '2024-06-01T12:30:01Z', true
+ );
+
+ // Run before the limit is reached
+ harness.expectState('2024-06-01T12:30:02Z', timeLimitsManager, TimeLimitsState.ACTIVE);
+ harness.expectState('2024-06-01T13:30:00Z', timeLimitsManager, TimeLimitsState.ACTIVE);
+
+ // Change session limits part-way through the day in a way which means
+ // the child no longer has time left that day
+ harness.addTimerChildProxyEstimatedTimesChangedEvent(
+ '2024-06-01T13:45:00Z',
+ true,
+ TestHarness.timeStrToSecs('2024-06-01T10:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-01T13:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-02T00:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-02T03:00:00Z')
+ );
+ harness.addParentalControlsManagerSessionLimitsChangedEvent(
+ '2024-06-01T13:45:01Z', true
+ );
+
+ // Run before the limit is over
+ harness.expectState('2024-06-01T13:45:02Z', timeLimitsManager, TimeLimitsState.LIMIT_REACHED);
+ harness.expectState('2024-06-01T14:00:00Z', timeLimitsManager, TimeLimitsState.LIMIT_REACHED);
+
+ // Change session limits after the child has run out of time in a way
+ // which means the child has still run out of time
+ harness.addTimerChildProxyEstimatedTimesChangedEvent(
+ '2024-06-01T14:30:00Z',
+ true,
+ TestHarness.timeStrToSecs('2024-06-01T10:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-01T14:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-02T00:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-02T04:00:00Z')
+ );
+ harness.addParentalControlsManagerSessionLimitsChangedEvent(
+ '2024-06-01T14:30:01Z', true
+ );
+
+ // Run before the limit is over
+ harness.expectState('2024-06-01T14:30:02Z', timeLimitsManager, TimeLimitsState.LIMIT_REACHED);
+ harness.expectState('2024-06-01T15:00:00Z', timeLimitsManager, TimeLimitsState.LIMIT_REACHED);
+
+ // Change session limits after the child has run out of time in a way
+ // which means the child has more time remaining
+ harness.addTimerChildProxyEstimatedTimesChangedEvent(
+ '2024-06-01T15:30:00Z',
+ true,
+ TestHarness.timeStrToSecs('2024-06-01T10:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-01T16:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-02T00:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-02T06:00:00Z')
+ );
+ harness.addParentalControlsManagerSessionLimitsChangedEvent(
+ '2024-06-01T15:30:01Z', true
+ );
+
+ // Run until the limit is reached
+ harness.expectState('2024-06-01T15:30:02Z', timeLimitsManager, TimeLimitsState.ACTIVE);
+ harness.expectState('2024-06-01T16:00:05Z', timeLimitsManager, TimeLimitsState.LIMIT_REACHED);
+
+ // Run before the limit is over
+ harness.expectState('2024-06-01T16:00:06Z', timeLimitsManager, TimeLimitsState.LIMIT_REACHED);
+ harness.expectState('2024-06-01T23:59:59Z', timeLimitsManager, TimeLimitsState.LIMIT_REACHED);
+
+ // After the limit is over, GetEstimatedTimes will return new values of
+ // the current and next sessions, so change the values accordingly
+ harness.addTimerChildProxyEstimatedTimesChangedEvent(
+ '2024-06-02T00:00:00Z',
+ false,
+ TestHarness.timeStrToSecs('2024-06-02T00:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-02T06:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-03T00:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-03T06:00:00Z')
+ );
+ harness.addParentalControlsManagerSessionLimitsChangedEvent(
+ '2024-06-02T00:00:01Z', true
+ );
+
+ // Run after the limit is over
+ harness.expectState('2024-06-02T00:00:02Z', timeLimitsManager, TimeLimitsState.ACTIVE);
+
+ harness.shutdownManager('2024-06-02T01:00:00Z', timeLimitsManager);
+
+ harness.run();
+ });
+
+ it('records usage correctly to the malcontent timer daemon', () => {
+ const harness = new TestHarness({
+ 'org.gnome.desktop.screen-time-limits': {
+ 'history-enabled': false,
+ 'daily-limit-enabled': true,
+ 'daily-limit-seconds': 4 * 60 * 60,
+ },
+ }, null, true);
+ harness.initializeMockClock('2024-06-01T10:00:00Z');
+ harness.addTimerChildProxyEstimatedTimesChangedEvent(
+ '2024-06-01T10:00:01Z',
+ false,
+ TestHarness.timeStrToSecs('2024-06-01T10:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-01T20:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-02T00:00:00Z'),
+ TestHarness.timeStrToSecs('2024-06-02T10:00:00Z')
+ );
+ harness.addParentalControlsManagerSessionLimitsChangedEvent(
+ '2024-06-01T10:00:02Z', true
+ );
+ const timeLimitsManager = harness.createTimeLimitsManager();
+
+ // Session limits changed
+ harness.addParentalControlsManagerSessionLimitsChangedEvent(
+ '2024-06-01T11:00:00Z', true
+ );
+ harness.expectUsage('2024-06-01T11:00:01Z', '2024-06-01T10:00:00Z', '2024-06-01T11:00:00Z');
+
+ // User state changed
+ harness.addLoginUserStateChangeEvent('2024-06-01T12:00:00Z', 'lingering', true);
+ harness.expectUsage('2024-06-01T12:00:01Z', '2024-06-01T10:00:00Z', '2024-06-01T12:00:00Z');
+
+ harness.addLoginUserStateChangeEvent('2024-06-01T13:00:00Z', 'active', false);
+ harness.expectUsage('2024-06-01T13:00:01Z', '2024-06-01T10:00:00Z', '2024-06-01T12:00:00Z');
+
+ harness.addLoginUserStateChangeEvent('2024-06-01T14:00:00Z', 'offline', true);
+ harness.expectUsage('2024-06-01T14:00:01Z', '2024-06-01T13:00:00Z', '2024-06-01T14:00:00Z');
+
+ harness.addLoginUserStateChangeEvent('2024-06-01T15:00:00Z', 'active', false);
+ harness.expectUsage('2024-06-01T15:00:01Z', '2024-06-01T13:00:00Z', '2024-06-01T14:00:00Z');
+
+ // Disable wellbeing & parental
+ harness.addParentalControlsManagerSessionLimitsChangedEvent(
+ '2024-06-01T16:00:00Z', false
+ );
+ harness.expectUsage('2024-06-01T16:00:01Z', '2024-06-01T15:00:00Z', '2024-06-01T16:00:00Z');
+
+ harness.addParentalControlsManagerSessionLimitsChangedEvent(
+ '2024-06-01T17:00:00Z', true
+ );
+ harness.expectUsage('2024-06-01T17:00:01Z', '2024-06-01T15:00:00Z', '2024-06-01T16:00:00Z');
+
+ // Shutdown
+ harness.shutdownManager('2024-06-01T18:00:00Z', timeLimitsManager);
+ harness.expectUsage('2024-06-01T18:00:00Z', '2024-06-01T17:00:00Z', '2024-06-01T18:00:00Z');
+
+ harness.run();
+ });
+
it('tracks usage correctly from an existing history file', () => {
const harness = new TestHarness({
'org.gnome.desktop.screen-time-limits': {